package com.zjl.media.core.ffmpeg;

import com.zjl.media.core.MediaTransferContext;
import com.zjl.media.core.MediaTransferTask;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * @author linfuxin Created on 2022-03-01 16:35:29
 */
@Slf4j
@Getter
public class FfmpegTask implements MediaTransferTask {

    private final FfmpegContext context;

    public FfmpegTask(FfmpegContext ffmpegContext) {
        this.context = ffmpegContext;
        initCommand(ffmpegContext);
    }

    /**
     * 注意：命令与参数不能设置为一个整体的字符串，要不然执行会有各种问题，确保一个参数一个集合对象
     */
    private List<String> command;

    @Override
    public void run() {
        Process process = null;
        try (ServerSocket tcpServer = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
            tcpServer.setSoTimeout(context.getSocketTimeout());
            String socketUrl = resolveSocketUrl(tcpServer);
            command.add(socketUrl);
            log.info(String.join(" ", command));

            process = new ProcessBuilder(command).start();
            // 无论windows还是linux 都必须执行一次，才能正常播放视频(windows不必执行err.readLine，linux必须执行err.readLine)
            readProcessStream(process);
            doPushClient(tcpServer);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (process != null) {
                process.destroy();
            }
            closeClient();
        }
    }

    /**
     * https://blog.csdn.net/qq_40351360/article/details/125284210
     * https://www.cnblogs.com/MacrossFT/p/12038479.html
     *
     * @param process
     */
    private void readProcessStream(Process process) {
        Thread inThread = new Thread(() -> {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                for (; ; ) {
                    String info = in.readLine();
                    if (log.isDebugEnabled()) {
                        log.debug("processStream info:{}", info);
                    }
                    if (info == null) {
                        break;
                    }
                }
            } catch (IOException e) {
                log.error("process reader in ex!", e);
            }
        });
        inThread.setDaemon(true);
        inThread.start();

        Thread errThread = new Thread(() -> {
            try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                for (; ; ) {
                    String errInfo = err.readLine();
                    if (log.isDebugEnabled()) {
                        log.debug("processStream err:{}", errInfo);
                    }
                    if (errInfo == null) {
                        break;
                    }
                }
            } catch (IOException e) {
                log.error("process reader in ex!", e);
            }
        });
        errThread.setDaemon(true);
        errThread.start();
    }

    private void doPushClient(ServerSocket tcpServer) {
        try (
                Socket socketClient = tcpServer.accept();
                ByteArrayOutputStream bos = context.getOutput();
                DataInputStream input = new DataInputStream(socketClient.getInputStream());
        ) {
            if (log.isDebugEnabled()) {
                log.debug("start push Client!");
            }
            // 视频越清晰调整越大
            byte[] buffer = new byte[context.getBufferSize()];
            int len;

            for (; ; ) {
                len = input.read(buffer);
                if (len == -1) {
                    break;
                }
                bos.write(buffer, 0, len);
                if (context.getHeader() == null) {
                    context.setHeader(bos.toByteArray());
                    context.getHeaderLatch().countDown();
                    bos.reset();
                    for (ChannelHandlerContext client : context.getClients()) {
                        client.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(context.getHeader())));
                    }
                    continue;
                }

                // 帧数据
                byte[] data = bos.toByteArray();
                bos.reset();

                // 发送到前端
                sendFrame(data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void sendFrame(byte[] data) {
        for (ChannelHandlerContext client : context.getClients()) {
            if (!client.channel().isWritable()) {
                // 直接跳过
                continue;
            }
            client.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(data)));
        }
    }

    private void closeClient() {
        if (context == null || CollectionUtils.isEmpty(context.getClients())) {
            return;
        }
        for (ChannelHandlerContext client : context.getClients()) {
            client.close();
        }
        context.setClients(null);
    }

    private String resolveSocketUrl(ServerSocket tcpServer) {
        return "tcp://" +
                tcpServer.getInetAddress().getHostAddress() +
                ":" +
                tcpServer.getLocalPort();
    }

    private FfmpegTask appendCommand(String arg) {
        command.add(arg);
        return this;
    }

    private void initCommand(FfmpegContext ffmpegContext) {
        command = new ArrayList<>();

        if (ffmpegContext.getFfmpegCommandEnum() == FfmpegCommandEnum.WIN_INTEL_SOFT) {
            appendWinIntelSoft(ffmpegContext);
        } else if (ffmpegContext.getFfmpegCommandEnum() == FfmpegCommandEnum.WIN_INTEL_HARD) {
            appendWinIntelHard(ffmpegContext);
        } else if (ffmpegContext.getFfmpegCommandEnum() == FfmpegCommandEnum.LINUX_INTEL_SOFT) {
            appendLinuxIntelSoft(ffmpegContext);
        } else if (ffmpegContext.getFfmpegCommandEnum() == FfmpegCommandEnum.LINUX_INTEL_HARD) {
            appendLinuxIntelHard(ffmpegContext);
        }
    }

    /**
     * ffmpeg -rtsp_transport tcp -i rtsp://admin:zjl12345@192.168.1.50:554/Streaming/Channels/1 -timeout 5000000 -max_delay 1 -c:v libopenh264
     * -c:a aac -pix_fmt yuv420p -crf 28 -threads 4 -flvflags no_duration_filesize -s 1280x720 -f flv tcp://127.0.0.1:18039
     *
     * @param ffmpegContext 上下文
     */
    private void appendWinIntelSoft(MediaTransferContext ffmpegContext) {
        appendCommand("ffmpeg")
                .appendCommand("-rtsp_transport").appendCommand("tcp").appendCommand("-i").appendCommand(ffmpegContext.getRtsp())
                .appendCommand("-timeout").appendCommand("5000000") // 5s超时
                .appendCommand("-max_delay").appendCommand("1")
                .appendCommand("-c:v").appendCommand("libx264")
                .appendCommand("-c:a").appendCommand("aac")
                .appendCommand("-pix_fmt").appendCommand("yuv420p").appendCommand("-crf").appendCommand("28")
                .appendCommand("-threads").appendCommand("4").appendCommand("-flvflags").appendCommand("no_duration_filesize")
                .appendCommand("-s").appendCommand("640x360").appendCommand("-f").appendCommand("flv");
    }

    /**
     * ffmpeg -rtsp_transport tcp -i rtsp://admin:zjl12345@192.168.1.50:554/Streaming/Channels/1 -timeout 5000000 -max_delay 1 -c:v libopenh264
     * -c:a aac -pix_fmt yuv420p -crf 28 -threads 4 -flvflags no_duration_filesize -s 1280x720 -f flv tcp://127.0.0.1:18039
     *
     * @param ffmpegContext 上下文
     */
    private void appendLinuxIntelSoft(MediaTransferContext ffmpegContext) {
        appendCommand("ffmpeg")
                .appendCommand("-rtsp_transport").appendCommand("tcp").appendCommand("-i").appendCommand(ffmpegContext.getRtsp())
                .appendCommand("-timeout").appendCommand("5000000") // 5s超时
                .appendCommand("-max_delay").appendCommand("1")
                .appendCommand("-c:v").appendCommand("libx264")
                .appendCommand("-c:a").appendCommand("aac")
                .appendCommand("-pix_fmt").appendCommand("yuv420p").appendCommand("-crf").appendCommand("28")
                .appendCommand("-threads").appendCommand("4").appendCommand("-flvflags").appendCommand("no_duration_filesize")
                .appendCommand("-s").appendCommand("640x360").appendCommand("-f").appendCommand("flv");
    }

    /**
     * ffmpeg -vaapi_device /dev/dri/renderD128 -rtsp_transport tcp -i rtsp://admin:zjl12345@192.168.2.12:554/Streaming/Channels/1 -max_delay 1 -g 25 -r 25 -c:v h264_vaapi
     * -preset:v ultrafast -c:v copy -strict -2 -tune:v zerolatency -c:a aac -pix_fmt yuv420p -crf 28 -threads 4 -flvflags no_duration_filesize -s 1280x720 -f flv tcp://127.0.0.1:1389
     *
     * @param ffmpegContext 上下文
     */
    private void appendLinuxIntelHard(MediaTransferContext ffmpegContext) {
        appendCommand("ffmpeg")
                .appendCommand("-vaapi_device").appendCommand("/dev/dri/renderD128")
                .appendCommand("-rtsp_transport").appendCommand("tcp").appendCommand("-i").appendCommand(ffmpegContext.getRtsp())
                .appendCommand("-timeout").appendCommand("5000000") // 5s超时
                .appendCommand("-max_delay").appendCommand("1")
                .appendCommand("-c:v").appendCommand("h264_vaapi")
                .appendCommand("-preset:v").appendCommand("ultrafast")
                .appendCommand("-c:v").appendCommand("copy")
                .appendCommand("-strict").appendCommand("-2")
                .appendCommand("-tune:v").appendCommand("zerolatency")
                .appendCommand("-c:a").appendCommand("aac")
                .appendCommand("-pix_fmt").appendCommand("yuv420p").appendCommand("-crf").appendCommand("28")
                .appendCommand("-threads").appendCommand("4").appendCommand("-flvflags").appendCommand("no_duration_filesize")
                .appendCommand("-s").appendCommand("1280x720").appendCommand("-f").appendCommand("flv");
    }

    /**
     * ffmpeg -rtsp_transport tcp -i rtsp://admin:zjl12345@192.168.2.12:554/Streaming/Channels/1 -max_delay 1 -g 25 -r 25 -c:v h264_qsv
     * -preset:v ultrafast -c:v copy -strict -2 -tune:v zerolatency -c:a aac -pix_fmt yuv420p -crf 28 -threads 4 -flvflags no_duration_filesize -s 1280x720 -f flv tcp://127.0.0.1:1389
     *
     * @param ffmpegContext 上下文
     */
    private void appendWinIntelHard(MediaTransferContext ffmpegContext) {
        appendCommand("ffmpeg")
                .appendCommand("-rtsp_transport").appendCommand("tcp").appendCommand("-i").appendCommand(ffmpegContext.getRtsp())
                .appendCommand("-timeout").appendCommand("5000000") // 5s超时
                .appendCommand("-max_delay").appendCommand("1")
                .appendCommand("-c:v").appendCommand("h264_qsv")
                .appendCommand("-preset:v").appendCommand("ultrafast")
                .appendCommand("-c:v").appendCommand("copy")
                .appendCommand("-strict").appendCommand("-2")
                .appendCommand("-tune:v").appendCommand("zerolatency")
                .appendCommand("-c:a").appendCommand("aac")
                .appendCommand("-pix_fmt").appendCommand("yuv420p").appendCommand("-crf").appendCommand("28")
                .appendCommand("-threads").appendCommand("4").appendCommand("-flvflags").appendCommand("no_duration_filesize")
                .appendCommand("-s").appendCommand("1280x720").appendCommand("-f").appendCommand("flv");
    }
}
